package dht.node;

import java.io.IOException;
import java.math.BigInteger;
import java.net.*;
import java.util.*;

import dht.communication.CommunicationDHT;
import dht.data.*;
import dht.threads.*;
import dht.tools.Couple;
import dht.tools.Duo;
import dht.tools.Md5Impl;


/**
 * Node correspond au point d'entree de la DHT, il rejoint le reseau puis lance ses threads.
 * @author bouceka
 *
 */
public class Node extends Thread {

	/**
	 * Les jobs get/put que l'on peut donner au noeud
	 */
	public static Jobs jobs=new Jobs();

	public static int NB_DIMENSION;
	public static final String PLUS_INF =  "99999999999999999999999999999999999999999999999999999999999999";
	public static final String MOINS_INF = "-99999999999999999999999999999999999999999999999999999999999999";
	public static final BigInteger B_PLUS_INF = new BigInteger(Node.PLUS_INF);
	public static final BigInteger B_MOINS_INF = new BigInteger(Node.MOINS_INF);
	public static boolean init;

	//Contexte du noeud
	public static Contexte myContext = new Contexte(null,null,null,null,null);

	// Les donnees d'un noeud de la DHT.
	public static ArrayList<Contexte> contexteNonFusion;
	public static int dimensionCut;
	public static String adresseIpConnu;
	public static InetAddress monAdresse;

	// Threads de gestion des requetes
	private ManageGet threadGet = new ManageGet();
	private ManagePut threadPut = new ManagePut();
	private ManageMajVoisins threadMaj = new ManageMajVoisins();
	private ManageJoin threadJoin = null;
	private ManageQuit threadQuit = null;
	public boolean readyWork=false;

	/**
	 * Code d'initialisation du noeud : rejoindre le reseau, receptionner la DHT locale, etc...
	 * @throws IOException 
	 */
	private void initNode(boolean init) throws IOException{

		Node.dimensionCut = 0;
		Node.contexteNonFusion = new ArrayList<Contexte>();
		Node.init = init;

		Node.monAdresse = InetAddress.getByName(CommunicationDHT.getLocalIp());
		Node.myContext.setAdresseIP(Node.monAdresse);

		if(init){ // Si c'est le noeud root alors la dht correspond a ce noeud seulement.

			Node.myContext.setZoneLocale(new ArrayList<Couple<BigInteger>>());
			Node.myContext.setDhtLocale(new HashMap<TabBigInteger, DhtData<Integer>>());
			Node.myContext.setVoisinage(new ArrayList<ArrayList<Couple<String>>>());

			for(int i=0; i< 2*Node.NB_DIMENSION; i++)
				Node.myContext.getVoisinage().add(new ArrayList<Couple<String>>());

			Node.myContext.setVirtualID("");

			synchronized(Node.myContext){
				for(int i=0; i< Node.NB_DIMENSION; i++){
					Node.myContext.getZoneLocale().add(0, new Couple<BigInteger>(Node.B_MOINS_INF,Node.B_PLUS_INF));
				}
			}

			this.threadJoin = new ManageJoin(true);
			this.threadQuit = new ManageQuit(true);


		}
		else{ // Sinon on envoie un message JOIN dans le reseau.

			this.threadJoin = new ManageJoin(false);
			this.threadQuit = new ManageQuit(false);	
		}

		return;
	}


	public Node(int nbDimension,boolean init,String adresseIpConnu) throws IOException{

		Node.NB_DIMENSION = nbDimension;
		Node.adresseIpConnu = adresseIpConnu;

		this.initNode(init);
	}

	public void work(){

		// Lancement des threads.
		threadGet.start();
		threadPut.start();
		threadJoin.start();
		threadQuit.start();
		threadMaj.start();

		//ici on est le noeud root donc direct OK
		this.readyToWork();

		/*while(true){ // Laisser les threads travailler et dormir
			try {
				Thread.sleep(Long.MAX_VALUE);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}*/
	}

	public void readyToWork() {
		synchronized(this.threadGet.isBusy) {
			threadGet.isBusy=Boolean.FALSE;
		}
		synchronized(this.threadPut.isBusy) {
			threadPut.isBusy=Boolean.FALSE;
		}
		this.readyWork=true;
	}

	public void StopWorking() {
		synchronized(this.threadGet.isBusy) {
			threadGet.isBusy=Boolean.TRUE;
		}
		synchronized(this.threadPut.isBusy) {
			threadPut.isBusy=Boolean.TRUE;
		}
	}

	@Override
	public void run() {
		// Lancement des threads.
		threadGet.start();
		threadPut.start();
		threadJoin.start();
		threadQuit.start();
		threadMaj.start();

		if(Node.init == false){
			while (threadJoin.initFinished!=true) {
				try {
					sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}

		this.readyToWork();
		while (true) {
			boolean b=true;
			while (b) {
				synchronized(jobs) {
					if (jobs.getGetJobs().size()!=0 || jobs.getPutJobs().size()!=0) {
						b=false;
					}
				} 
				if (b!=false) {
					try {
						sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					continue;
				}

				synchronized (jobs) {
					if (jobs.getGetJobs().size()!=0) {
						this.get(jobs.getGetJobs().poll());
					}
					if (jobs.getPutJobs().size()!=0) {
						Duo<TabBigInteger,DhtData<Integer>> d=jobs.getPutJobs().poll();
						this.put(d.getFirst(),d.getSecond());
					}
				}
			}
		}


	}

	/**
	 * Fonction utilis�e pour quitter la DHT
	 */
	public void QUIT() {
		this.threadQuit.quitter_dht();

		this.threadGet.interrupt();
		this.threadJoin.interrupt();
		this.threadMaj.interrupt();
		this.threadPut.interrupt();

		System.exit(0);
	}

	/**
	 * Fonction put du noeud, privée
	 * @param poll la donnée à put
	 */
	private void put(TabBigInteger d,DhtData<Integer> data) {
		this.threadPut.putJob(new Duo<TabBigInteger,DhtData<Integer>>(d,data));
	}


	/**
	 * Fonction Get du noeud, donne le job au thread
	 * @param poll les coordonnées du point
	 */
	private void get(TabBigInteger poll) {
		this.threadGet.putJob(poll);
	}

	/**
	 * Fonction put asynchrone n'assurant pas forc�ment l'ordre
	 * @param data la donnee a envoyer
	 */
	public void async_PUT(ArrayList<String> criteres,DhtData<Integer> data) {
		synchronized(jobs.getPutJobs()) {
			jobs.getPutJobs().add(new Duo<TabBigInteger,DhtData<Integer>>(Md5Impl.creerTab(criteres),data));
		}
	}

	/**
	 * Fonction de demande si les put sont finis
	 * @return un tableau du maximum de reponse
	 */
	public ArrayList<Boolean> finished_PUT() {
		ArrayList<Boolean> ret=new ArrayList<Boolean>();
		synchronized(jobs.getPutAnswers()) {
			ret.addAll(jobs.getPutAnswers());
			jobs.getPutAnswers().clear();
		}
		return ret;
	}

	/**
	 * Fonction put du client, publique
	 * @param data la donnée à envoyer
	 * @return vrai si la donnée à bien été stockée
	 */
	public boolean PUT(ArrayList<String> criteres,DhtData<Integer> data) {
		synchronized(jobs.getPutJobs()) {
			jobs.getPutJobs().add(new Duo<TabBigInteger,DhtData<Integer>>(Md5Impl.creerTab(criteres),data));
		}
		boolean b=true;
		while (b) {
			synchronized(jobs.getPutAnswers()) {
				if (jobs.getPutAnswers().size()!=0) {
					b=false;
				}
			}

			if (b!=false) {
				try {
					sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		boolean ret=false;
		synchronized(jobs.getPutAnswers()) {
			ret=jobs.getPutAnswers().poll();
		}
		return ret;
	}

	/**
	 * Fonction GET async
	 * @param coord les coordonnées de la donnée à localiser
	 */
	public void async_GET(ArrayList<String> criteres) {
		synchronized(jobs.getGetJobs()) {
			jobs.getGetJobs().add(Md5Impl.creerTab(criteres));
		}
	}

	/**
	 * Fonctiond de retour asynchrone des get 
	 * @return le maximum de reponses get recues
	 */
	public ArrayList<DhtData<Integer>> finished_GET() {
		ArrayList<DhtData<Integer>> ret=new ArrayList<DhtData<Integer>>();
		synchronized (jobs.getGetAnswers()) {
			ret.addAll(jobs.getGetAnswers());
			jobs.getGetAnswers().clear();
		}
		return ret;
	}

	/**
	 * Fonction GET du client, publique
	 * @param coord les coordonnées de la donnée à localiser
	 * @return la donnée si présente, null sinon
	 */
	public DhtData<Integer> GET(ArrayList<String> criteres) {
		synchronized(jobs.getGetJobs()) {
			jobs.getGetJobs().add(Md5Impl.creerTab(criteres));
		}
		boolean b=true;
		while (b) {
			synchronized(jobs.getGetAnswers()) {
				if (jobs.getGetAnswers().size()!=0) {
					b=false;
				}
			}
			if (b!=false) {
				try {
					sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		DhtData<Integer> ret=null;
		synchronized(jobs.getGetAnswers()) {
			ret=jobs.getGetAnswers().poll();
		}
		return ret;
	}

	/**
	 * Fonction de réponse à un put que le thread va appeller
	 * @param b vrai si le put est OK
	 */
	public static void putPutReply(Boolean b) {
		synchronized (jobs.getPutAnswers()) {
			jobs.getPutAnswers().add(b);
		}
	}
	/**
	 * Fonction de réponse à un get que le thread va appeller
	 * @param b La donnée ou null si MISS
	 */
	public static void putGetReply(DhtData<Integer> b) {
		synchronized (jobs.getGetAnswers()) {
			jobs.getGetAnswers().add(b);
		}
	}

	public static void main(String[] args) {

		Node mySelf = null;

		System.out.println("yep !");
		if(args.length < 3){
			System.out.println("Usage : PgmDHT dimension [true|false] AddressIP");
			System.exit(1);
		}
		else{

			int dim = Integer.parseInt(args[0]);
			boolean init = Boolean.parseBoolean(args[1]);
			String adrrIp = args[2];

			try {
				mySelf = new Node(dim,init,adrrIp); 
			} catch (UnknownHostException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}

			mySelf.work();
		}
		return;
	}
}